<?php
/**
* All authentication and authorization tasks are carried out through this
* static class.
*
* Copyright (c) 2009 James Gauld <james@jamesgauld.net>
* This file is part of Scribble.
* @license http://www.scribblecms.co.uk/license.txt
*
* @package Scribble
*/
use Buan\Config;
use Buan\Model;
use Buan\ModelCollection;
use Buan\ModelCriteria;
use Buan\ModelManager;
class ScribbleAuth {

	/**
	* Holds all defined privileges, indexed by privilege identifier.
	*			
	* @var array
	*/
	private static $privileges = array();

	/**
	* Authenticates a user from the given credentials and returns a matching
	* UserModel instance, or FALSE if authentication failed.
	*
	* @param string Email address
	* @param string Unencrypted password
	* @return UserModel|FALSE
	*/
	public static function authenticate($email, $password) {
		$tp = Config::get('ext.Scribble.dbTablePrefix');
		$sql = "SELECT * FROM {$tp}user WHERE email=? AND password=md5(concat(hashsalt, ?)) LIMIT 1";
		$rec = ModelManager::sqlQuery($sql, array($email, $password))->fetch(PDO::FETCH_ASSOC);
		if($rec) {
			$user = Model::create('User');
			$user->populateFromArray($rec);
			$user->isInDatabase(TRUE);
			$user->hasChanged(FALSE);
			return $user;
		}
		else {
			return FALSE;
		}
	}

	/**
	* Test if a user/role has the specified privilege.
	*
	* Optionally, by including the third parameter in this method, you can test
	* if the user/role has the privilege on a specific object.
	*
	* If $tgtResource is an array, then it is treated as a parent-child path with
	* priority going from start to end in the array. So, for example, if the
	* authorize check fails on the first model in the array then it will test on
	* the next in the queue and so on until the end.
	*
	* If, after testing on all model in the $tgtResource array, the result is still
	* a failure then one final test is done on the "global" resource which is
	* a special resource on which global privileges are defined.
	*
	* @param UserModel|RoleModel
	* @param string Privilege to test
	* @param ScribbleModel|array
	*/
	public static function authorize($role, $priv, $tgtResource=NULL) {

		// If a User has been passed in the $roleOrUser parameter, then we will test
		// each of their Roles until authorization is found, or we run out of roles
		if($role instanceof UserModel) {
			$roles = $role->findRelatives('Role');
			foreach($roles as $role) {
				if(self::authorize($role, $priv, $tgtResource)) {
					return TRUE;
				}
			}
			return FALSE;
		}
		else if(!($role instanceof RoleModel)) {
			return FALSE;
		}

		// Authorize the global privilege
		if($tgtResource===NULL) {
			$tp = Config::get('ext.Scribble.dbTablePrefix');
			$sql = "SELECT id FROM {$tp}role_privileges WHERE role_id=?
							AND resource_id=?
							AND (pallow='*' OR FIND_IN_SET(?, pallow))";
			if(ModelManager::sqlQuery($sql, array($role->id, 'global', $priv))->fetch(PDO::FETCH_OBJ)) {
				return TRUE;
			}
			return FALSE;
		}

		// Authorize the privilege against a single resource, or path of resources
		else {
			$tgtResource = is_array($tgtResource) ? $tgtResource : array($tgtResource);
			foreach($tgtResource as $k=>$r) {
				$tgtResource[$k] = $r instanceof ScribbleModel ? $r->id : $r;
			}
			$tgtResource[] = 'global';
			$tp = Config::get('ext.Scribble.dbTablePrefix');
			$sql = "SELECT *, FIND_IN_SET(resource_id, ?) AS weight FROM {$tp}role_privileges WHERE role_id=? HAVING weight>0 ORDER BY weight";
			$stmt = ModelManager::sqlQuery($sql, array(implode(",", $tgtResource), $role->id));
			while($rec = $stmt->fetch(PDO::FETCH_OBJ)) {
				if($rec->pallow==='*' || in_array($priv, explode(",", $rec->pallow), TRUE)) {
					$stmt->closeCursor();
					return TRUE;
				}
				else if(in_array($priv, explode(",", $rec->pdeny), TRUE)) {
					$stmt->closeCursor();
					return FALSE;
				}
			}
			return FALSE;
		}
	}

	/**
	* Define a new privilege so it can be used in the cms.
	*
	* "Scenario tags" are a means by which you can tell Scribble that a privilege
	* is relevant to a particular scenario (or several scenarios). This provides
	* a simple way to add your own privileges to the list of core privileges in
	* any scenario.
	*
	* For example, the page-editor UI will display privileges belonging to the
	* "page-edit" scenario. So if you want your own privilege to appear in that UI
	* simply define it and include the "page-edit" scenario.
	*
	* @param string Privilege category
	* @param string Privilege identifier
	* @param string User friendly version of the identifier
	* @param string Description of the privilege
	* @param string Comma separated list of scenario tags
	* @return void
	*/
	public static function definePrivilege($category, $priv, $privName, $privDesc, $tags='') {
		$p = new StdClass();
		$p->id = $priv;
		$p->name = $privName;
		$p->description = $privDesc;
		$p->category = $category;
		$p->tags = $tags=='' ? array() : explode(",", $tags);
		self::$privileges[$p->id] = $p;
	}

	/**
	* Returns all the privileges that have been defined via ::definePrivilege().
	* Each element in the returned array is indexed by the privilege identifer.
	*
	* @return array
	*/
	public static function getDefinedPrivileges() {
		return self::$privileges;
	}

	/**
	* Returns the same array as ::getDefinedPrivileges(), but this time each
	* element in the array is a category of privileges and is indexed by the
	* category name.
	*
	* @return array
	*/
	public static function getDefinedPrivilegesByCategory() {
		$privs = self::getDefinedPrivileges();
		$result = array();
		foreach($privs as $p) {
			if(!isset($result[$p->category])) {
				$result[$p->category] = array();
			}
			$result[$p->category][$p->id] = $p;
		}
		return $result;
	}

	/**
	* Returns the same array as ::getDefinedPrivileges(), but this time each
	* element in the array is a list of privileges belonging to a particular
	* scenario and is indexed by the scenario tag.
	*
	* @param string If set then only privileges with this tag are returned
	* @return array
	*/
	public static function getDefinedPrivilegesByScenario($tag=NULL) {
		$privs = self::getDefinedPrivileges();
		$result = array();
		foreach($privs as $p) {
			foreach($p->tags as $t) {
				if(!isset($result[$t])) {
					$result[$t] = array();
				}
				$result[$t][$p->id] = $p;
			}
		}
		return $tag!==NULL && isset($result[$tag]) ? $result[$tag] : $result;
	}

	/**
	* Returns a list of RolePrivilegesModel instances that exist for the specified
	* resource, along with the privileges they are "allowed" and "denied".
	*
	* @param ScribbleModel|string A model or a model's ID
	* @return Buan\ModelCollection
	*/
	public static function getPrivilegeAssignments($tgtResource) {

		if(empty($tgtResource)) {
			return new ModelCollection();
		}

		$tgtResource = $tgtResource===NULL ? 'global' : (
			$tgtResource instanceof ScribbleModel ? $tgtResource->id : (string)$tgtResource
		);

		$tblPrefix = Config::get('ext.Scribble.dbTablePrefix');
		$c = new ModelCriteria();
		$c->addClause(ModelCriteria::EQUALS, 'resource_id', $tgtResource);
		$assignments = ModelManager::select('RolePrivileges', $c);

		return $assignments;
	}

	/**
	* Return a list of privileges (from self::$privileges) that the specified
	* Role has on the specified target resource (or the "global" resource if none
	* is specified).
	*
	* The returned array will either be empty (indicating the role has no
	* privileges on the resource) or index by the privilege identifier.
	*
	* @param RoleModel
	* @param ScribbleModel|string A model or a model's ID
	* @return array
	*/
	public static function getRolePrivileges($role, $tgtResource=NULL) {
		$tgtResource = $tgtResource===NULL ? 'global' : (
			$tgtResource instanceof ScribbleModel ? $tgtResource->id : (string)$tgtResource
		);
		$tp = Config::get('ext.Scribble.dbTablePrefix');
		$sql = "SELECT pallow FROM {$tp}role_privileges WHERE role_id=? AND resource_id=?";
		$stmt = ModelManager::sqlQuery($sql, array($role->id, $tgtResource));
		$privileges = array();
		while($rec = $stmt->fetch(PDO::FETCH_OBJ)) {
			if($rec->pallow=='*') {
				$stmt->closeCursor();
				return self::$privileges;
			}
			$pallow = explode(",", $rec->pallow);
			foreach($pallow as $pa) {
				$privileges[$pa] = self::$privileges[$pa];
			}
		}
		return $privileges;
	}

	/**
	* Grant privileges to the specified role.
	*
	* @param RoleModel
	* @param string Privileges to grant (comma separated or '*' for all privileges)
	* @param ScribbleModel|string A model or a model's ID
	* @return bool
	*/
	public static function grant($role, $priv, $tgtResource=NULL) {

		// Ignore blank privileges
		if(empty($priv)) {
			return TRUE;
		}

		// If no specific target models have been passed, default to the global
		// "global" resource.
		$resourceId = $tgtResource===NULL ? 'global' : (
			$tgtResource instanceof ScribbleModel ? $tgtResource->id : (string)$tgtResource
		);

		// Create a new entry for the "role_privileges" table
		$rp = Model::create('RolePrivileges');
		$rp->addRelatives($role);
		$rp->pallow = $priv;
		$rp->resource_id = 'global';
		if($rp->getModelManager()->save($rp)) {
			return TRUE;
		}
		else {
			return FALSE;
		}
	}

	/**
	* Revoke a role's privileges.
	*
	* @param RoleModel
	* @param string Privileges to revoke (comma-separated or '*' for all privileges)
	* @param ScribbleModel
	* @return bool
	*/
	public static function revoke($role, $priv, $tgtResource=NULL) {
		$resourceId = $tgtResource===NULL ? 'global' : (
			$tgtResource instanceof ScribbleModel ? $tgtResource->id : (string)$tgtResource
		);
		$tp = Config::get('ext.Scribble.dbTablePrefix');

		// Remove all privileges
		// We just remove the whole record from role_privileges
		if($priv=='*') {
			$sql = "DELETE FROM {$tp}role_privileges WHERE role_id=? AND resource_id=?";
			ModelManager::sqlQuery($sql, array($role->id, $resourceId));
		}

		// Remove specified privileges
		else {
			// TODO
		}

		// Result
		return TRUE;
	}
}
?>